原始题目:剑指 Offer 40. 最小的k个数 (opens new window)

# 方法一:快速排序

如果要求最小的 kk 个数,那么将数组从小到大排序,然后取前 kk 个数字就行了。但是因为题目没要求返回的数组是有序的,因此,只要找到最小的 kk 个数就行了,不用管元素的顺序。

(以下假设所有的元素不重复,数组名为 numsnums

这里可以借助快速排序中 partitionpartition 操作返回的索引 i 来快速找到第 k 大的元素。假设数组在区间 [l,r][l, r] 中,经过 partitionpartition 操作返回哨兵索引 ii,那么

  • [l,i)[l, i) 区间内的元素都小于 nums[i]nums[i]
  • (i,r](i, r] 区间内的元素都大于 nums[i]nums[i]

此时,如果

  • i=ki = k,那么 [l,i)[l, i) 区间内的元素就是最小的 kk 个数,返回数组的前 kk 个数;
  • i>ki > k,说明 kk[l,i)[l, i) 区间内,对 [l,i1][l, i-1] 区间进行快速排序;
  • i<ki < k,说明 kk(i,r](i, r] 区间内,对 [i+1,r][i+1, r] 区间进行快速排序;

循环上述操作,直到找到 i=ki = k

代码:

public int[] getLeastNumbers(int[] arr, int k) {
    // 先排序再取前k个
    if (k >= arr.length) return arr;
    return quickSort(arr, 0, arr.length - 1, k);
}

/**
 * 使用用快排
 */
private int[] quickSort(int[] arr, int l, int r, int k) {
    int i = l, j = r;
    while (i < j) {
        while (i < j && arr[j] >= arr[l]) j--;
        while (i < j && arr[i] <= arr[l]) i++;
        swap(arr, i, j);
    }
    swap(arr, i, l);
    if (i > k) {
        return quickSort(arr, l, i - 1, k);
    }
    if (i < k) {
        return quickSort(arr, i + 1, r, k);
    }
    return Arrays.copyOf(arr, k);
}

private void swap(int[] arr, int i, int j) {
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

复杂度分析

  • 时间复杂度O(N)O(N)NN 为数组的元素数量。每轮递归中,都是根据 kkii 的关系选择递归的,由于 ii 分布的随机性,则向下递归子数组的平均长度为 N2\frac{N}{2} 。因此平均情况下,哨兵划分操作一共有 NN + N2\frac{N}{2} + N4\frac{N}{4} + N8\frac{N}{8} + ... + NN\frac{N}{N} = 2N12N-1 。因此时间复杂度为 O(N)O(N)

  • 空间复杂度O(logN)O(logN) :划分函数的平均递归深度为 O(logN)O(logN)

# 方法二:大顶堆

根据大顶堆的特点,父节点比左右子节点都大,因此一个大小为 kk 的大顶堆 maxHeapmaxHeap ,其根节点是最大的。

我们先把数组的前 kk 个元素插入到 maxHeapmaxHeap 中,然后接下来的元素,不断地跟 maxHeapmaxHeap 根节点对对比,如果比 根节点小,那么就把根节点弹出,然后把新的元素插入到 maxHeapmaxHeap 中,直到所有元素都遍历了。maxHeapmaxHeap 中的元素就是最小的 kk 个数。

代码:

public int[] getLeastNumbers(int[] arr, int k) {
	return priority(arr, k);
}

/**
 * 试用大顶堆,存放前 k 个最小数
 */
private int[] priority(int[] arr, int k) {
    if (arr == null || k <= 0) {
        return new int[]{};
    }
    PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.comparingInt(x -> (int) x).reversed());
    for (int i = 0; i < k; i++) {
        maxHeap.offer(arr[i]);
    }
    for (int i = k; i < arr.length; i++) {
        if (maxHeap.peek() > arr[i]) {
            maxHeap.poll();
            maxHeap.offer(arr[i]);
        }
    }
    int[] ans = new int[maxHeap.size()];
    int i = 0;
    while (!maxHeap.isEmpty()) {
        ans[i++] = maxHeap.poll();
    }
    return ans;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

复杂度分析

  • 时间复杂度O(NlogK)O(NlogK)NN 为数组中的元素个数,KK 为大顶堆的大小。最坏情况下需要对每个元素都进行入堆,每次入堆的时间为 O(logK)O(logK)
  • 空间复杂度O(K)O(K)KK 为输入参数 kk 的大小。大顶堆需要占用空间 O(K)O(K)
上次更新: 2023/10/15